/* 
 * [New BSD License (BSD 3-Clause License)]
 * Copyright (c) 2012, Max Bechtold
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     - Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     - Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     - Neither the name of the Rel2Xml Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package maxbe.rel2xml.generator.dml;

import static maxbe.rel2xml.util.Config.getBoolean;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import maxbe.rel2xml.generator.Column;
import maxbe.rel2xml.generator.Column.Mapping;
import maxbe.rel2xml.generator.docs.DataType;
import maxbe.rel2xml.util.Config;

/**
 * This class represents an SQL table update expression.</br> Note that unlike
 * in SQL XQuery updates in form of an replace-value-of-node-expression can only
 * update a single node, i.e. column. Thus, multiple assignments in an update
 * clause require an replace-node-expression with element content
 * reconstruction.</br> Special care must be taken for columns than can contain
 * null values as they might require insert instead of replace expressions.</br>
 * Another complication arises from the fact that a subtree of a tupel whose
 * mapping determining foreign key is changed has to be moved to another part of
 * the document (as the tupel it belongs to is now a different one). This
 * requires an insert operation followed by a delete operation.
 * 
 * @author Max Bechtold
 * 
 */
public class Update extends AbstractDmlInstance {

	/** Table to be updated */
	private Entity target;

	/** The list of assignments of this update expression */
	private List<Assignment> assignments;

	private final static List<String> operators = Arrays.asList("+", "-", "*",
			"/", "%", "||");

	public Update(String rootNodePath) {
		this.rootNodePath = rootNodePath;
		assignments = new ArrayList<Assignment>();
	}

	public Entity getTarget() {
		return target;
	}

	public List<Assignment> getAssignments() {
		return assignments;
	}

	public void setTarget(Entity target) {
		this.target = target;
	}

	public void addAssignment(Assignment assignment) {
		assignments.add(assignment);
	}

	@Override
	protected String declareExtVars() {
		List<String> variables = new ArrayList<String>();
		String varDecl = super.declareExtVars(variables);
		for (Assignment assignment : assignments) {
			int index = 0;
			String value = assignment.getValue();

			// Make sure computations are based on typed values
			boolean typed = false;
			for (String op : operators) {
				if (value.contains(op)) {
					typed = true;
					break;
				}
			}

			while ((index = value.indexOf('?')) != -1) {
				String var = getVar(assignment.getColumn().getName(), variables);
				String format = (typed && assignment.getColumn().getColumn()
						.getDataType() == DataType.INTEGER) ? "xs:integer($%s)"
						: "$%s";
				value = value.substring(0, index) + String.format(format, var)
						+ value.substring(index + 1, value.length());
				varDecl += String.format("declare variable $%s external;\n",
						var);
			}
			assignment.setValue(value);
		}
		return varDecl;
	}

	@Override
	public String toXQuery() {
		String update = super.toXQuery();
		String extVars = declareExtVars();
		if (!extVars.isEmpty())
			update += extVars;

		String context = String
				.format("%s%s%s",
						rootNodePath,
						target.getTable().getXPathSelector(),
						(predicate != null ? '[' + predicate.toXQuery(true) + ']'
								: ""));

		update += "declare variable $ctx := " + context + ";\n\n";

		boolean newParent = false;

		String condition = "";
		for (Assignment assignment : assignments) {
			if (assignment.getColumn().isImplicit()) {
				if (!condition.isEmpty())
					condition += " and ";
				condition += String.format("$ctx/%s = %s", assignment
						.getColumn().getXmlName(), assignment.getValue());
				newParent = true;
			}
		}

		if (newParent) {
			update += getConditionalExpression(condition);
		} else if (assignments.size() == 1) {
			// Construct replace-value-of-node-expression
			update += getUpdateColumnExpression(assignments.get(0), false, "") + ';';
		} else if (getBoolean(Config.SPLIT_UPDATES)) {
			String expressions = "";
			// Construct multiple replace-value-of-node-expressions
			for (Assignment assignment : assignments) {
				if (!expressions.isEmpty())
					expressions += "\n\n";
				expressions += getUpdateColumnExpression(assignment, false, "") + ';';
			}
			update += expressions;
		} else {
			// Construct replace-node-expression using transforms
			String modify = "";

			for (Assignment assignment : assignments) {
				if (!modify.isEmpty())
					modify += ",\n";
				modify += getUpdateColumnExpression(assignment, true, "\t\t");
			}

			update += String
					.format("replace node $ctx\nwith\n\tcopy $new := $ctx\n\tmodify (\n%s\n\t)\n\treturn $new;",
							modify);
		}

		return update;
	}

	private String getConditionalExpression(String condition) {
		// Ugliest possible case: Construct if-then-else with replaces, insert,
		// and delete expressions

		String update = String.format("if (%s)\nthen (\n", condition);

		String replaceExp = "";
		for (Assignment assignment : assignments)
			if (!assignment.getColumn().getColumn().isImplicit()) {
				if (!replaceExp.isEmpty())
					replaceExp += ",\n\t";
				replaceExp += getUpdateColumnExpression(assignment, false, "\t");
			}

		update += replaceExp;

		update += "\n) else (\n\tinsert node";

		replaceExp = "";
		for (Assignment assignment : assignments) {
			if (!assignment.getColumn().getColumn().isImplicit()
					|| !getBoolean(Config.IMPLICIT_FKEYS)) {
				if (!replaceExp.isEmpty())
					replaceExp += ",\n";
				replaceExp += getUpdateColumnExpression(assignment, true,
						"\t\t\t");
			}
		}

		if (replaceExp.isEmpty())
			update += "\n\t\t$ctx";
		else
			update += String
					.format("\n\t\tcopy $new := $ctx\n\t\tmodify (\n%s\n\t\t)\n\t\treturn $new",
							replaceExp);

		condition = "";
		for (Assignment assignment : assignments) {
			if (assignment.getColumn().isImplicit()) {
				if (!condition.isEmpty())
					condition += " and ";
				condition += String.format("%s = %s", assignment.getColumn()
						.getRefColumn().getXmlName(), assignment.getValue());
			}
		}

		update += String.format("\n\tinto\n\t\t%s%s[%s]/%ss", rootNodePath,
				target.getTable().getParent().getXPathSelector(), condition,
				target.getTable().getName());

		condition = "";
		for (Assignment assignment : assignments) {
			if (assignment.getColumn().isImplicit()) {
				if (!condition.isEmpty())
					condition += " and ";
				condition += String.format("%s <> %s", assignment.getColumn()
						.getRefColumn().getXmlName(), assignment.getValue());
			}
		}

		update += String
				.format(",\n\tdelete node\n\t\t%s%s[%s]%s%s",
						rootNodePath,
						target.getTable().getParent().getXPathSelector(),
						condition,
						target.getTable().getXPathSelector(true),
						(predicate != null ? '[' + predicate.toXQuery(true) + ']'
								: ""));
		return update + "\n)";
	}

	private String getUpdateColumnExpression(Assignment assignment,
			boolean inTransform, String indent) {
		String context = (inTransform ? "$new" : "$ctx");
		String exp = null;

		if (!assignment.getColumn().getColumn().isNotNull()
				&& getBoolean(Config.OMIT_NULLS)) {
			if (assignment.getValue().equalsIgnoreCase("null")) {
				exp = String.format("%sdelete node %s/%s", indent, context,
						assignment.getColumn().getXmlName());
			} else {
				// Check for node existence using fn:exists()
				String replace = getReplaceValueOfNodeExp(indent + '\t',
						context, assignment.getColumn().getXmlName(),
						assignment.getValue(context), true);
				String insert = String.format("%s\tinsert node %s into %s",
						indent, getColumnConstructor(assignment), context);

				exp = String
						.format("%sif (fn:exists(%s/%s))\n%sthen (\n%s\n%s) else (\n%s\n%s)",
								indent, context, assignment.getColumn()
										.getXmlName(), indent, replace, indent,
								insert, indent);
			}
		} else {
			if (assignment.getValue().equalsIgnoreCase("null")) // &&
																// !getBooleanProperty(Config.OMIT_NULLS)
				exp = getReplaceValueOfNodeExp(indent, context, assignment
						.getColumn().getXmlName(), "\"\"", inTransform);
			else
				exp = getReplaceValueOfNodeExp(indent, context, assignment
						.getColumn().getXmlName(),
						assignment.getValue(context), inTransform);
		}

		return exp;
	}

	private String getReplaceValueOfNodeExp(String indent, String context,
			String column, String value, boolean oneLine) {
		return String.format("%sreplace value of node %s/%s%swith %s", indent,
				context, column, (oneLine ? " " : "\n" + indent), value);
	}

	private String getColumnConstructor(Assignment assignment) {
		// Note: Column references are not concerned here as they would evaluate
		// to the empty sequence anyway (as their is no node for the column
		// present)
		String constructor = null;
		Column column = assignment.getColumn().getColumn();

		if (column.getMapping() == Mapping.XML_ATTRIBUTE)
			constructor = "attribute {'%s'} {%s}";
		else
			constructor = "element {'%s'} {%s}";

		return String.format(constructor, assignment.getColumn().getName(),
				assignment.getValue());
	}

	@Override
	public String toSQL() {
		String setClause = "";
		for (Assignment assignment : assignments) {
			if (!setClause.isEmpty())
				setClause += ", ";
			setClause += assignment.toString();
		}

		String where = null;
		if (predicate != null)
			where = predicate.toSql();

		return String.format("update %s\nset %s", target.getTable().getName(),
				setClause) + (where != null ? "\nwhere " + where : "");
	}

	@Override
	public String toString() {
		return toSQL();
	}

}
